package serveur;

/**
 * Describe class MainServer here.
 *
 *
 * Created: Wed Feb 10 18:27:34 2010
 * Modified : Mon Apr 25 17:35:26 2011
 *
 * @author <a href="mailto:francois@ubuntu">FM</a>
 * @version 1.0
 */

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ServerSocketChannel;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import common.HtmlFormatter;

public final class MainServer {

    /**
     * Creates a new <code>MainServer</code> instance.
     *
     */
    static int NB_THREADS = 20;
    static int PORT_BASE_DEFAUT = 3000;
    static int userServiceCpt;
    static int annonceurServiceCpt;
    
    private static final String LOG_FILE_PREFIX_NAME = "server-output";
    private static final String LOG_LEVEL_KEY = "log.filterLevel";		
	private static final String LOG_FORMATTERS_KEY = "log.formatters";	
	
	private static Level  logLevel;
	
	private static FileHandler logFileTxt;
	private static FileHandler logFileHTML;
	
	private static Properties conf = new Properties();

    // TODO mettre ces constantes dans un fichier properties de config
    static private String agentId="1234";
    static private String agentVersion="0.0";
    static private String agentMS="Master";
    



    private static final Logger logger = Logger.getLogger(MainServer.class.getName());
    private static ExecutorService pool;
    private boolean mServerOn;

    private CanalUserHandler mCanalUser;
    private CanalAdminHandler mCanalAdmin;
    public static MainServer m_mainServer;
    private MonitorConnexion mMonitor = new MonitorConnexion();
    private int debitCanalUser;

    
    /**
     * CONSTRUCTEUR
     * @param poolsize
     * @param portbase
     */
    public MainServer(int poolsize, int portbase) {
    	mServerOn = true;
    	pool = Executors.newFixedThreadPool(poolsize);
    	mCanalUser = new CanalUserHandler(portbase, pool);
    	mCanalAdmin  = new CanalAdminHandler(portbase+2, pool);
    	mMonitor.start();
    	mCanalUser.start();
    	mCanalAdmin.start();
    }

    
    
    /**
     * METHODES
     */
    public void halt(){
    	System.out.println("On passe dans la methode halt");
    	pool.shutdown();
    	try{
    	    pool.awaitTermination(60,TimeUnit.SECONDS);
    	}catch(InterruptedException ex){
    		logger.log(Level.SEVERE,"Unexpected error",ex);
    	}
    	mCanalUser.interrupt();
    	mServerOn = false;
    }
    
    void interruptAdmin(){
    	mCanalAdmin.interrupt();
    }
    
    boolean isServerOn(){
    	return mServerOn;
    }

        
    /**
     * GETTERS & SETTERS
     */
    static String getMasterOrSlave(){
    	return agentMS;
    }

    static String getId(){
    	return agentId;
    }

    static String getVersion(){
    	return agentVersion;
    }

	static Level getLogLevel(){
       	if (logLevel == null) {
       		String level = conf.getProperty(LOG_LEVEL_KEY,"INFO").toUpperCase().trim();
       		logLevel = Level.parse(level);
       	}
    	return logLevel;
    }
    
    static void initLogger(){
        logger.setLevel(getLogLevel());
        String logFormatters = conf.getProperty(LOG_FORMATTERS_KEY,"HTML,TXT,CONSOLE");
        StringTokenizer st = new StringTokenizer(logFormatters,",; ");
        String formatter;
        while (st.hasMoreTokens()) {
        	formatter = st.nextToken().toUpperCase().trim();
        	if (formatter.equals("HTML")) {
        		try {
        			logFileHTML = new FileHandler(LOG_FILE_PREFIX_NAME+".html");
            		logFileHTML.setFormatter(new HtmlFormatter());
            		logger.addHandler(logFileHTML);  
        		} catch (Exception ex) {
        			logger.log(Level.SEVERE, "Unable to create log file: "+LOG_FILE_PREFIX_NAME+".html", ex);
        		} 
        	}
        	if (formatter.equals("TXT")) {
        		try {
        			logFileTxt = new FileHandler(LOG_FILE_PREFIX_NAME+".txt");
            		logFileTxt.setFormatter(new SimpleFormatter());
            		logger.addHandler(logFileTxt);  
        		} catch (Exception ex) {
        			logger.log(Level.SEVERE, "Unable to create log file: "+LOG_FILE_PREFIX_NAME+".txt", ex);
        		}      		
        	}
        }
	}
	
	
	//////////////////////////////////////
	//									//
	//		CLASSES INTERNES			//
	//									//
	//////////////////////////////////////
	/**
	 * MonitorConnexion
	 * 
	 */
	public class MonitorConnexion extends Thread {
		boolean activ;
		int cptcour;
		int cptpre;
		
		MonitorConnexion(){
		    activ = true;
		}
		
		MonitorConnexion(int canalUserCpt){
		    activ = true;
		    
		}
				
		public void run(){
		    while (activ){
		    	cptpre = cptcour;
		    	cptcour = mCanalUser.getCpt();
//		    	cptcour = MainServer.getCanalUser().getCpt();
		    	debitCanalUser = (cptcour-cptpre);
		    	//logger.log(Level.INFO,"connexion user " + debitCanalUser + "/s");
		    	try{
		    		sleep(1000);
		    	}catch(InterruptedException ex){
		    		logger.log(Level.SEVERE,"Unexpected error",ex);
		    	}
		    }
		}
		
		void terminate(){
		    activ = false;
		}
		
		int getDebitCanalUser() {
			return debitCanalUser;
		}
	}
	
	
	/**
	 * CanalUserHandler
	 * Port de base 3000 -  Gere les connexions et les echanges de messages
	 */
	class CanalUserHandler  extends Thread{
		
		private ServerSocketChannel mServerSocket;
		private final ExecutorService mPool;
		private int mCpt = 0;

		CanalUserHandler(int port, ExecutorService pool){
		    this.mPool = pool;
		    try {
		    	// ouverture du port sur le serveur
		    	mServerSocket = ServerSocketChannel.open();
		    	// association IP du serveur au port
		    	mServerSocket.socket().bind(new InetSocketAddress(port));
		    	logger.info("C'est bon Start CanalUserHandler on port " + port );
		    }catch(IOException ex){
		    	logger.severe("C'est pas bon Start CanalUserHandler on port " + port);
		    	System.exit(1);
		    }
		}

		public void run(){
		    Socket sock;
		    try {
		    	while (isServerOn()) {
		    		try{
		    			// accepte les connections sur ce cannal
		    			sock =  mServerSocket.accept().socket();
		    			System.out.println("sock");
		    			// incremente le nombre d'utilisateurs connectes
		    			MainServer.userServiceCpt++;
		    			// TODO - Definir la classe UserServerDelegate
		    			mPool.execute(new UserServerDelegate(sock,mCpt++, MainServer.this, mPool ));
		    		}catch(ClosedByInterruptException ex){
		    			logger.log(Level.SEVERE,"Unexpected error",ex);
		    		}
		    	}
		    } catch (IOException ex) {
			    logger.log(Level.SEVERE,"Unexpected error",ex);
			    mPool.shutdown();
			}
		}
		
		int getCpt(){
		    return mCpt;
		}
	}

	
	/**
	 * CanalAnnonceurHandler
	 * Gere les transfert de données avec le controle de flux
	 */
	class CanalAnnonceurHandler  extends Thread{

		private ServerSocketChannel mServerSocket;
		private InetSocketAddress mInetSocketAddress;
		private final ExecutorService mPool;
		private AnnonceurServerDelegate mAnnonceurServerDelegate; 
		
		private int mPort;
		CanalAnnonceurHandler(ExecutorService pool){
		    this.mPool = pool;
			try {
				// Steph : C'est le systeme qui choisi le port
				mInetSocketAddress = new InetSocketAddress(0);
				
				mServerSocket = ServerSocketChannel.open();
		    	mServerSocket.socket().bind(mInetSocketAddress);
		    	
		    	// Steph : Recuperation du port surlequel le canal annouceur a ete ouvert.
		    	mPort = mServerSocket.socket().getLocalPort();
		    	logger.log(Level.INFO,"Start CanalAnnonceurHandler on port " + mPort );
		    }catch(IOException ex){
//		    	logger.log(Level.SEVERE, e);
		    	logger.log(Level.SEVERE,"Unexpected error",ex);
		    	System.exit(1);
		    }
		}
		// Steph : Methode pour avoir le port sur lequel le canal Annouceur a ete ouvert 
		public int getLocalPort(){
			return mPort;
		}
		
		// Steph : Methode pour avoir acces au annouceurServerDelegate
		public AnnonceurServerDelegate getAnnonceurServerDelegate(){
			return mAnnonceurServerDelegate;
		}
		
		public void run(){
		    Socket sock;
	    	logger.log(Level.INFO,"* run CanalAnnonceurHandler *" );
		    try {
		    	while (isServerOn()) {
		    		try{
		    			sock =  mServerSocket.accept().socket();
		    			MainServer.annonceurServiceCpt++;
		    			mAnnonceurServerDelegate = new AnnonceurServerDelegate(sock);
		    		}catch(ClosedByInterruptException ex){
		    			logger.log(Level.SEVERE,"Unexpected error",ex);
		    		}
		    	}
		    } catch (IOException ex) {
		    	logger.log(Level.SEVERE,"Unexpected error",ex);
		    	mPool.shutdown();
		    }
		}
	}
	
	
	/**
	 * CanalAdminHandler
	 * 
	 */
	class CanalAdminHandler  extends Thread{

		private ServerSocketChannel mServerSocket;
		private final ExecutorService mPool;
		private AdminServerDelegate mDelegate;
		private int mServiceCpt = 0;
		private int mPort;

		CanalAdminHandler(int port, ExecutorService pool){
		    logger.log(Level.INFO,"Start CanalAdminHandler on port " + port);
		    this.mPool = pool;
		    this.mPort = port;
		    try {
		    	mServerSocket = ServerSocketChannel.open();
		    	mServerSocket.socket().bind(new InetSocketAddress(port));
		    }catch(IOException ex) {
		     logger.log(Level.SEVERE,"Unexpected error",ex);
		     System.exit(1);}
		}

		public void run(){
		    Socket sock = null;
			while (isServerOn()) {
			    try{
			    	sock =  mServerSocket.accept().socket();
			    	logger.info("connexion accepted with " + sock.getInetAddress());
			    	mServiceCpt++;
			    	// 	TODO - Definir la classe AdminServerDelegate et faire le lien avec la classe MainServer
			    	mDelegate = new AdminServerDelegate(sock,MainServer.this);
			    	mDelegate.runAdmin();
			    }catch(ClosedByInterruptException e){
			    	interrupted(); // acquitement de l'interrupt
			    	logger.info("end admin");
			    	System.out.println("end admin");
			    } catch (IOException ex) {
			    	logger.log(Level.SEVERE,"Unexpected error",ex);
			    	mPool.shutdown();
			    }
			}
			logger.log(Level.INFO,"end admin delegate");
			System.out.println("end admin delegate");
		}	
	}
	
	
	//////////////////////////////////////
	//									//
	//				MAIN				//
	//									//
	//////////////////////////////////////
	/**
     *
     * Describe <code>main</code> method here.
     *
     * @param args a <code>String</code> value
     */
    public static void main(final String[] args) {
    	int portBase = PORT_BASE_DEFAUT;

    	// Chargement du fichier de configuration
        // Initialise les configurations du serveur a partir d'un fichier de configuration
   		InputStream is;
   		InputStreamReader isr;
   		BufferedReader br;
   		String ligne;
   		String fileConf = "server.conf";
    				
   		// Test de la fonction execute
   		logger.log(Level.INFO, "Chargement de la configuration du serveur à paritr du fichier : "+fileConf);
    		
   		// Lecture du fichier d'authentification
   		try {
   			initLogger();
   			// creation du flux de fichier
   			is = new FileInputStream(fileConf);
   			isr = new InputStreamReader(is);
   			br = new BufferedReader(isr);

   			// recuperation de la configuration
   			while ((ligne=br.readLine()) != null){
   				String[] temp = ligne.split("=");
   				// PORT
   				if((temp[0].equals("port")) && (!temp[1].isEmpty())){
   					// Test si le parametre est un entier puis si le port est compris entre 0 et 65535
   					if((Integer.valueOf(temp[1])>=0) && (Integer.valueOf(temp[1])<=65535)){
   						portBase = Integer.valueOf(temp[1]);
   						logger.log(Level.INFO, "PORT = "+portBase);
   					} else {
   						logger.log(Level.WARNING, "PORT INCORRECT VALUE (DEFAUT = "+portBase+")");
   					}
   				}
   				// NB_THREADS
   				if((temp[0].equals("nbThreads")) && (!temp[1].isEmpty())){
   					// Test si le parametre est un entier puis si le nombre est compris entre 20 et 100
   					if((Integer.valueOf(temp[1])>=20) && (Integer.valueOf(temp[1])<=100)){
   						NB_THREADS = Integer.valueOf(temp[1]);
   						logger.log(Level.INFO, "NB_THREADS = "+NB_THREADS);
   					} else {
   						logger.log(Level.WARNING, "NB_THREADS INCORRECT VALUE (DEFAUT = "+NB_THREADS+")");
   					}
   				}   				
   				// agentID
   				if((temp[0].equals("agentID")) && (!temp[1].isEmpty())){
					agentId = temp[1];
					logger.log(Level.INFO, "agentId = "+agentId);
   				}
   				// agentVersion
   				if((temp[0].equals("agentVersion")) && (!temp[1].isEmpty())){
					agentVersion = temp[1];
					logger.log(Level.INFO, "agentVersion = "+agentVersion);
   				}
   				// agentMS
   				if((temp[0].equals("agentMS")) && (!temp[1].isEmpty())){
					agentMS = temp[1];
					logger.log(Level.INFO, "agentMS = "+agentMS);
   				}
   			}
   			// Fermeture du flux
   			is.close();
    				
   		} catch (FileNotFoundException e) {
   			// TODO Auto-generated catch block
   			e.printStackTrace();
   		} catch (IOException e) {
   			// TODO Auto-generated catch block
   			e.printStackTrace();
   		}

   		// Prise en compte du port passé en parametre au demarrage du serveur
    	if (args.length > 0){
    		try{
    			if ((Integer.parseInt(args[0])>=0) && (Integer.parseInt(args[0])<=65535)){
    				portBase = Integer.parseInt(args[1]);
    				logger.log(Level.INFO, "PORT BY COMMAND LINE = "+portBase);
    			}
    		}catch(Exception e){}
    	}
    	
//    	Object sync = new Object();
    	MyShutDown ShutDown = new MyShutDown();
    	Runtime.getRuntime().addShutdownHook(ShutDown);
    	try{
    		MainServer.m_mainServer = new MainServer(NB_THREADS, portBase);
    		MainServer.m_mainServer.mCanalUser.join();	// Attend la fin du thread canalUser
    		logger.info("Diffuser user server halted");
    		MainServer.m_mainServer.mMonitor.terminate();
    		MainServer.m_mainServer.mCanalAdmin.join();	// Attend la fin du thread canalAdmin
        } catch (Exception e) {
            logger.log(Level.INFO,"Diffuser: " + e.toString());
            e.printStackTrace();
        } finally {
        	logger.info("Diffuser server halted\n");
        } 
           
        logger.log(Level.INFO,"Diffuser server halted\n");
        System.out.println("Diffuser server halted\n");
    }
    
    public void terminer(){
    	mCanalUser.interrupt();
    }
}

class MyShutDown extends Thread {
	public void run(){
		MainServer.m_mainServer.halt();
	}
}